package org.copper.ui
{
	import flash.display.Bitmap;
	import flash.display.BitmapData;
	import flash.display.DisplayObject;
	import flash.display.Sprite;
	import flash.events.Event;
	import flash.events.MouseEvent;
	import flash.geom.Rectangle;
	
	import org.copper.objectpool.ObjectPool;
	import org.copper.ui.events.CListInScrollRangeEvent;
	import org.copper.ui.events.CListMaxScrollpositionReachedEvent;
	import org.copper.ui.events.CListMinScrollPositionReachedEvent;
	import org.copper.utils.ConditionUtil;

	public class BaseCList extends CSprite
	{
		//this flag is set to 'true' when scroller is on its edge (left, right, top or buttom) position.
		private var m_isOnEdge:Boolean;
		//this flag is set to true whenever the scroller is not in its edge position. Exists only for performance reasons.
		private var m_isInRange:Boolean;
		//this rectangle represents currently shown scroll content. It can be litterally 'scrolrect' property when not scrolling, or bimap cropping endge.
		private var m_scrollRect:Rectangle;
		//the container sprite which contains all the objects.
		private var m_container:CSprite;
		//masked container for not scrolling state.
		private var m_maskedContainer:Sprite;
		//classic data provider model. Expected to be a vector.
		private var m_dataProvider:Object;
		//Object pool used for Item renderers recycling. Any instance of 'MySprite' can be used as an item renderer.
		private var m_objectPool:ObjectPool;
		//vector containig objects to be set on item renderers. 
		private var m_sharedProperty:Vector.<__________SharedProperty>;
		//optional mouse events provider. By default it is 'this' reference.
		private var m_swipeEventsProvider:DisplayObject;
		//Holds currently visible objects
		private var m_visibleObjects:Vector.<CSprite>;
		//the class of item renderer.
		private var m_itemRenderer:Class;
		//the bitmap canvas for scrolling.
		private var m_canvas:Bitmap;
		//current relative scroll position. from 0 to 1. 
		private var m_scrollPosition:Number = 0;
		//amount of pixels to regard as begining of swipe.
		private var m_motionThreshold:Number = 7.5;
		//flag is set to true when swipe gesture is begined.
		private var m_reportMouseMove:Boolean = false;
		//flag is set to true when swipe gesture is begined. (for perfomance reasons)
		private var m_showingCanvas:Boolean = false;
		//flag is set to false when swipe gesture is begined. (for perfomance reasons)
		private var m_showingSprite:Boolean = true;
		//minimal speed to begin enertion effect.
		private var m_minEnertionSpeed:Number = 0.5;
		//this coeficient determines enertion friction.
		private var m_enertionFrictionCoeficient:Number = 0.959;
		//this flag enables/disables the enertion.
		private var m_enertionEnabled:Boolean = true;
		//set to true when mouse is down.
		private var m_mouseIsDown:Boolean = false;
		//this flag defines panel to be rendered with bitmap blitting for external scrolling.
		private var m_scrollRenderMode:Boolean = false;
		
		//override of width and height to set those properties regardless to sprite properties.
		private var m_forcedWidth:Number = 0;
		private var m_forcedHeight:Number = 0;
		
		//properties defining scrolling bounds.
		private var m_minScroll:Number = 0;
		private var m_maxScroll:Number = 1;
		
		//last difference determine enertion speed.
		private var m_lastDifX:Number = 0;
		private var m_lastDifY:Number = 0;
		
		//prev mouse cordinates for calculating offsets.
		private var m_prevX:Number = 0;
		private var m_prevY:Number = 0;
		
		//handles scrollpositionChangeEvent
		private var m_scrollPositionChangedHandler:Function = null;
		
		public function BaseCList()
		{
			super();
			m_container = new CSprite();
			m_scrollRect = new Rectangle();
			m_maskedContainer = new Sprite();
			m_canvas = new Bitmap();
			m_dataProvider = new Vector.<Object>();
			m_sharedProperty = new Vector.<__________SharedProperty>();
			m_objectPool = new ObjectPool(itemRendererFactory, 0, "init", "recycle", "dispose");
			
			m_maskedContainer.addChild(m_container);
			addChild(m_maskedContainer);
			
			scrollX = 0;
			scrollY = 0;
			m_swipeEventsProvider = this;
			m_itemRenderer = CSprite;
			
			m_swipeEventsProvider.addEventListener(MouseEvent.MOUSE_DOWN, onMouseDown);
			m_swipeEventsProvider.addEventListener(MouseEvent.MOUSE_UP, onMouseUp);
			m_swipeEventsProvider.addEventListener(MouseEvent.MOUSE_MOVE, onMouseMove);
			
			m_isOnEdge = m_scrollPosition <= m_minScroll || m_scrollPosition >= m_maxScroll;
			m_isInRange = !m_isOnEdge;
		}
		
		public function get scrollPositionChangedHandler():Function
		{
			return m_scrollPositionChangedHandler;
		}
		
		public function set scrollPositionChangedHandler(value:Function):void
		{
			m_scrollPositionChangedHandler = value;
		}
		
		
		public function get scrollRenderMode():Boolean
		{
			return m_scrollRenderMode;
		}
		
		public function set scrollRenderMode(value:Boolean):void
		{
			m_prevX = stage.mouseX;
			m_prevY = stage.mouseY;
			if (value != m_scrollRenderMode)
			{
				m_scrollRenderMode = value;
				if (value)
				{
					showBitmap();
				}
				else
				{
					showContainer();
				}
			}
		}
		
		private function showBitmap():void
		{
			if (m_showingSprite)
			{
				removeChild(m_maskedContainer);
				m_showingSprite = false;
			}
			if (m_showingCanvas == false)
			{
				render();
				addChild(m_canvas);
				m_showingCanvas = true;
			}
		}
		
		private function showContainer():void
		{
			if (m_showingCanvas)
			{
				removeChild(m_canvas);
				m_showingCanvas = false;
			}
			m_maskedContainer.scrollRect = m_scrollRect;
			if (m_showingSprite == false)
			{
				addChild(m_maskedContainer);
				m_showingSprite = true;
			}
		}
		
		private function onMouseDown(event:MouseEvent):void
		{
			if (m_scrollRenderMode)
				return;
			
			if (m_enertionEnabled)
			{
				stopEnertion();
			}
			
			m_prevX = stage.mouseX;
			m_prevY = stage.mouseY;
			m_mouseIsDown = true;
		}
		
		private function onMouseUp(event:MouseEvent):void
		{
			if (m_scrollRenderMode)
				return;
			
			m_mouseIsDown = false;
			m_prevX = stage.mouseX;
			m_prevY = stage.mouseY;
			
			m_reportMouseMove = false;
			if (m_enertionEnabled)
			{
				startEnertion();
			}
			else
			{
				showContainer();
			}
		}
		
		private function startEnertion():void
		{
			if (m_lastDifX < m_minEnertionSpeed && m_lastDifY < m_minEnertionSpeed)
			{
				stopEnertion();
			}
			else
			{
				addEventListener(Event.ENTER_FRAME, onEnterFrame);
			}
		}
		
		private function onEnterFrame(event:Event):void
		{
			const minSpeed:Number = m_minEnertionSpeed;
			const coef:Number = m_enertionFrictionCoeficient;
			var ldx:Number = m_lastDifX;
			var ldy:Number = m_lastDifY;
			if(abs(ldx) >= minSpeed || abs(ldy) >= minSpeed)
			{
				ldx = ldx * coef;
				ldy = ldy * coef;
				m_lastDifX = ldx;
				m_lastDifY = ldy;
				onTransform(ldx, ldy);
			}
			else
			{
				stopEnertion();
			}
		}
		
		private function stopEnertion():void
		{
			try
			{
				removeEventListener(Event.ENTER_FRAME, onEnterFrame);
			}
			catch(err:Error){}
			
			showContainer();
		}
		
		private function abs(number:Number):Number
		{
			return number<0?-number:number;
		}
		
		private function onMouseMove(event:MouseEvent):void
		{
			if (m_scrollRenderMode)
				return;
			
			if (m_mouseIsDown == false)
				return;
			
			const curX:Number = stage.mouseX;
			const curY:Number = stage.mouseY;
			const difX:Number = m_prevX - curX;
			const difY:Number = m_prevY - curY;
			
			m_lastDifX = difX;
			m_lastDifY = difY;
			
			if (m_reportMouseMove)
			{
				onTransform(difX, difY);
				m_prevX = curX;
				m_prevY = curY;
			}
			else
			{
				if (abs(difX) > m_motionThreshold || abs(difY) > m_motionThreshold)
				{
					showBitmap();
					m_reportMouseMove = true;
				}
			}
		}
		
		
		private function itemRendererFactory():CSprite
		{
			const result:CSprite = (new m_itemRenderer()) as CSprite;
			ConditionUtil.assert(result != null, "Item renderer is not 'CSprite'");
			return result;
		}
		
		private function calculateVisible():void
		{
			const rect:Rectangle = m_scrollRect;
			const objects:Vector.<CSprite> = m_visibleObjects;
			const visLen:uint = objects.length;
			const container:CSprite = m_container;
			const len:int = container.numChildren;
			const lastIndex:int = len - 1;
			const aproximateIndex:int = Math.round(len * m_scrollPosition);
			const rx:Number = rect.x;
			const ry:Number = rect.y;
			const rw:Number = rx + rect.width;
			const rh:Number = ry + rect.height;
			var index:int = aproximateIndex;
			
			for (var i:uint = 0; i < visLen; i++) 
			{
				const objectToHide:CSprite = objects.shift();
				objectToHide.visible = false;
			}
			
			var objToShow:CSprite = container.getChildAt(index) as CSprite;
			
			while(index > 0 && checkBounds(objToShow, rx, ry, rw, rh))
			{
				objToShow.visible = true;
				objects.push(objToShow);
				index--;
				objToShow = container.getChildAt(index) as CSprite;
			}
			
			if (index == 0)
			{
				if (checkBounds(objToShow, rx, ry, rw, rh))
				{
					objToShow.visible = true;
					objects.push(objToShow);
				}
			}
			
			index = aproximateIndex + 1;
			if (index < len)
			{
				objToShow = container.getChildAt(index) as CSprite;
				if (index == lastIndex)
				{
					if (checkBounds(objToShow, rx, ry, rw, rh))
					{
						objToShow.visible = true;
						objects.push(objToShow);
					}
				}
				else
				{
					while(index < lastIndex && checkBounds(objToShow, rx, ry, rw, rh))
					{
						objToShow.visible = true;
						objects.push(objToShow);
						index++;
						objToShow = container.getChildAt(index) as CSprite;
					}
				}
			}
		}
		
		private function checkBounds(obj:DisplayObject, rx:Number, ry:Number, rw:Number, rh:Number):Boolean
		{
			const ox:Number = obj.x;
			const oy:Number = obj.y;
			
			return ox < rw && ox + obj.width > rx && oy < rh && oy + obj.height > ry;
		}
		
		public function get itemRenderer():Class
		{
			return m_itemRenderer;
		}
		
		public function set itemRenderer(value:Class):void
		{
			m_itemRenderer = value;
		}
		
		
		public function get scrollPosition():Number
		{
			return m_scrollPosition;
		}
		
		public function set scrollPosition(value:Number):void
		{
			value = value < m_minScroll?m_minScroll:value;
			value = value > m_maxScroll?m_maxScroll:value;
			
			if (value == m_scrollPosition)
				return;
			
			onScrollPositionUpdate(value, m_scrollPosition);
			m_scrollPosition = value;
			
			m_isOnEdge = false;
			
			if (value == m_minScroll)
			{
				dispatchEvent(new CListMinScrollPositionReachedEvent());
				m_isOnEdge = true;
				m_isInRange = false;
			}
			
			if (value == m_maxScroll)
			{
				dispatchEvent(new CListMaxScrollpositionReachedEvent());
				m_isOnEdge = true;
				m_isInRange = false;
			}
			
			if (m_isOnEdge == false)
			{
				if (m_isInRange == false)
				{
					m_isInRange = true;
					dispatchEvent(new CListInScrollRangeEvent());
				}
			}
			
			if (m_scrollPositionChangedHandler != null)
			{
				const norma:Number = value - m_minScroll;
				m_scrollPositionChangedHandler(norma/(m_maxScroll - m_minScroll));
			}
		}
		
		public function pop():Object
		{
			const result:Object = m_dataProvider.pop();
			onDataProviderUpdated(m_dataProvider, m_scrollPosition, m_container);
			return result;
		}
		
		public function push(obj:DisplayObject):uint
		{
			const result:uint = m_dataProvider.push(obj);
			onDataProviderUpdated(m_dataProvider, m_scrollPosition, m_container);
			return result;
		}
		
		public function shift():Object
		{
			const result:Object = m_dataProvider.shift();
			onDataProviderUpdated(m_dataProvider, m_scrollPosition, m_container);
			return result;
		}
		
		public function unshift(obj:DisplayObject):uint
		{
			const result:uint = m_dataProvider.unshift(obj);
			onDataProviderUpdated(m_dataProvider, m_scrollPosition, m_container);
			return result;
		}
		
		public function get swipeEventsProvider():DisplayObject
		{
			return m_swipeEventsProvider;
		}
		
		public function set swipeEventsProvider(value:DisplayObject):void
		{
			if (value == null)
				return;
			
			if (value != m_swipeEventsProvider)
			{
				m_swipeEventsProvider.removeEventListener(MouseEvent.MOUSE_DOWN, onMouseDown);
				m_swipeEventsProvider.removeEventListener(MouseEvent.MOUSE_UP, onMouseUp);
				m_swipeEventsProvider.removeEventListener(MouseEvent.MOUSE_MOVE, onMouseMove);
				m_swipeEventsProvider = value;
				m_swipeEventsProvider.addEventListener(MouseEvent.MOUSE_DOWN, onMouseDown);
				m_swipeEventsProvider.addEventListener(MouseEvent.MOUSE_UP, onMouseUp);
				m_swipeEventsProvider.addEventListener(MouseEvent.MOUSE_MOVE, onMouseMove);
			}
		}
		
		public function get dataProvider():Object
		{
			return m_dataProvider;
		}
		
		public function set dataProvider(value:Object):void
		{
			m_dataProvider = value;
			onDataProviderUpdated(m_dataProvider, m_scrollPosition, m_container);
		}
		
		public function get enertionEnabled():Boolean
		{
			return m_enertionEnabled;
		}
		
		public function set enertionEnabled(value:Boolean):void
		{
			m_enertionEnabled = value;
		}
		
		public function get enertionFrictionCoeficient():Number
		{
			return m_enertionFrictionCoeficient;
		}
		
		public function set enertionFrictionCoeficient(value:Number):void
		{
			m_enertionFrictionCoeficient = value;
		}
		
		public function get minEnertionSpeed():Number
		{
			return m_minEnertionSpeed;
		}
		
		public function set minEnertionSpeed(value:Number):void
		{
			m_minEnertionSpeed = value;
		}
		
		public function addSharedObject(propName:String, obj:Object):void
		{
			m_sharedProperty.push(new __________SharedProperty(propName, obj));
		}
		
		public function get maxScroll():Number
		{
			return m_maxScroll;
		}
		
		public function set maxScroll(value:Number):void
		{
			m_maxScroll = value;
		}
		
		public function get minScroll():Number
		{
			return m_minScroll;
		}
		
		public function set minScroll(value:Number):void
		{
			m_minScroll = value;
		}
		
		public function get motionThreshold():Number
		{
			return m_motionThreshold;
		}
		
		public function set motionThreshold(value:Number):void
		{
			m_motionThreshold = value;
		}
		
		
		
		
		//~ overrided section
		override public function dispose():void
		{
			m_swipeEventsProvider.removeEventListener(MouseEvent.MOUSE_DOWN, onMouseDown);
			m_swipeEventsProvider.removeEventListener(MouseEvent.MOUSE_UP, onMouseUp);
			m_swipeEventsProvider.removeEventListener(MouseEvent.MOUSE_MOVE, onMouseMove);
			removeAllFromContainer();
			m_objectPool.disposeAll();
			m_objectPool = null
			m_swipeEventsProvider = null;
			m_dataProvider = null;
			m_sharedProperty = null;
			
		}
		
		override public function set height(value:Number):void
		{
			m_forcedHeight = value;
			m_scrollRect.height = value;
			m_maskedContainer.scrollRect = m_scrollRect;
		}
		
		override public function get height():Number
		{
			return m_forcedHeight;
		}
		
		override public function set width(value:Number):void
		{
			m_forcedWidth = value;
			m_scrollRect.width = value;
			m_maskedContainer.scrollRect = m_scrollRect;
		}
		
		override public function get width():Number
		{
			return m_forcedWidth;
		}
		
		
		
		
		//~ protected section
		protected function removeAllFromContainer():void
		{
			const len:int = m_container.numChildren;
			for(var i:int = 0; i < len; i++)
			{
				m_objectPool.submit(m_container.removeChildAt(0));
			}
		}
		
		protected function set scrollX(value:Number):void
		{
			m_scrollRect.x = value;
			m_maskedContainer.scrollRect = m_scrollRect;
		}
		
		protected function set scrollY(value:Number):void
		{
			m_scrollRect.y = value;
			m_maskedContainer.scrollRect = m_scrollRect;
		}
		
		protected function get scrollX():Number
		{
			return m_scrollRect.x;
		}
		
		protected function get scrollY():Number
		{
			return m_scrollRect.y;
		}
		
		protected function getRenderer():CSprite
		{
			const object:Object = m_objectPool.require();
			
			const props:Vector.<__________SharedProperty> = m_sharedProperty;
			const len:uint = props.length;
			
			for (var i:uint = 0; i < len; i++) 
			{
				const p:__________SharedProperty = props[i];
				object[p.name] = p.value;
			}
			
			const spr:CSprite = object as CSprite;
			spr.visible = false;
			return spr;
		}
		
		protected function returnRenderer(renderer:CSprite):void
		{
			m_objectPool.submit(renderer);
		}
		
		protected function forceUpdate():void
		{
			onDataProviderUpdated(m_dataProvider, m_scrollPosition, m_container);
		}
		
		protected function render():void
		{
			var prevData:BitmapData = m_canvas.bitmapData;
			if (prevData != null)
			{
				prevData.dispose();
				prevData = null;
			}
			
			calculateVisible();
			
			m_canvas.bitmapData = m_container.snapShotRect(m_scrollRect);
		}
		
		
		//~ abstract section
		protected function onTransform(offsetX:Number, offsetY:Number):void
		{
			throw new Error("'onTransform' method has to be overrided.");
		}
		
		protected function onDataProviderUpdated(dp:Object, pos:Number, container:CSprite):void
		{
			throw new Error("'dataProviderUpdated' method has to be overrided.");
		}
		
		protected function onScrollPositionUpdate(newPosition:Number, oldPosition:Number):void
		{
			throw new Error("'onScrollPositionUpdate' method has to be overrided.");
		}
		
		public function get avarageItmScrollSize():Number
		{
			throw new Error("'avarageItmScrollSize' property has to be overrided.");
		}
	}
}

final class __________SharedProperty
{
	public var name:String;
	public var value:Object;
	
	
	public function __________SharedProperty(Name:String, Value:Object)
	{
		name = Name;
		value = Value;
	}
}